/*
 *  Copyright 2004 Blandware (http://www.blandware.com)
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package com.blandware.atleap.webapp.action.core.siteMap;

import com.blandware.atleap.common.Constants;
import com.blandware.atleap.common.util.QueryInfo;
import com.blandware.atleap.common.util.StringUtil;
import com.blandware.atleap.model.core.ActionPage;
import com.blandware.atleap.model.core.BaseObject;
import com.blandware.atleap.model.core.ContentDocument;
import com.blandware.atleap.model.core.ContentPage;
import com.blandware.atleap.model.core.Page;
import com.blandware.atleap.service.core.ContentResourceManager;
import com.blandware.atleap.service.core.PageManager;
import com.blandware.atleap.webapp.action.core.BaseAction;
import com.blandware.atleap.webapp.util.core.GlobalProperties;
import com.blandware.atleap.webapp.util.core.WebappConstants;
import com.blandware.atleap.webapp.util.core.WebappUtil;
import org.apache.commons.validator.GenericValidator;
import org.apache.struts.Globals;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;

/**
 * <p>Prepares site map to show on the page
 * </p>
 * <p><a href="ListContentPagesAction.java.html"><i>View Source</i></a></p>
 * <p/>
 *
 * @author Sergey Zubtsovskiy <a href="mailto:sergey.zubtsovskiy@blandware.com">&lt;sergey.zubtsovskiy@blandware.com&gt;</a>
 * @author Roman Puchkovskiy <a href="mailto:roman.puchkovskiy@blandware.com">
 *         &lt;roman.puchkovskiy@blandware.com&gt;</a>
 * @version $Revision: 1.14 $ $Date: 2007/03/24 16:02:39 $
 * @struts.action path="/core/siteMap/view"
 * validate="false"
 * @struts.action-forward name="viewSiteMap"
 * path=".core.siteMap.view"
 */
public final class ViewSiteMapAction extends BaseAction {
	/**
	 * @param mapping  The ActionMapping used to select this instance
	 * @param form     The optional ActionForm bean for this request (if any)
	 * @param request  The HTTP request we are proceeding
	 * @param response The HTTP response we are creating
	 * @return an ActionForward instance describing where and how
	 *         control should be forwarded, or null if response
	 *         has already been completed
	 */
	public ActionForward execute(ActionMapping mapping, ActionForm form,
	                             HttpServletRequest request, HttpServletResponse response) throws Exception {

		MapSection rootSection = null;
		// TODO: get from cache here

		if ( rootSection == null ) {
			// create root section
			rootSection = new MapSection("/");

			PageManager pageManager = (PageManager) getBean(Constants.PAGE_MANAGER_BEAN);
			ContentResourceManager resourceManager = (ContentResourceManager) getBean(Constants.CONTENT_RESOURCE_MANAGER_BEAN);

			// get locale identifier
			Locale locale = (Locale) request.getSession().getAttribute(Globals.LOCALE_KEY);
			String localeIdentifier = locale.getLanguage();
			Map queryParameters = new HashMap();
			queryParameters.put("localeIdentifier", localeIdentifier);
			QueryInfo queryInfo = new QueryInfo();
			queryInfo.setQueryParameters(queryParameters);
			queryInfo.setOrderByClause("r.uri asc");

			// get list of documents
			List documents = resourceManager.listContentDocuments(queryInfo).asList();

			// add active filter
			queryInfo.setWhereClause("page.active = 'T'");
            queryInfo.setOrderByClause("page.uri asc");

			// get list of content pages
			List contentPages = pageManager.listContentPages(queryInfo).asList();

			// get list of action pages
			List actionPages = pageManager.listActionPages(queryInfo).asList();

			// process each list

			// cp
			for ( Iterator i = contentPages.iterator(); i.hasNext(); ) {
				ContentPage contentPage = (ContentPage) i.next();
				String uri = contentPage.getUri();
				rootSection.addElement(contentPage, uri.substring(WebappConstants.CONTENT_PAGES_URI_PREFIX.length()), localeIdentifier, request);
			}

			// ap
			for ( Iterator i = actionPages.iterator(); i.hasNext(); ) {
				ActionPage actionPage = (ActionPage) i.next();
				String uri = actionPage.getUri();
				rootSection.addElement(actionPage, uri, localeIdentifier, request);
			}

			// documents
			for ( Iterator i = documents.iterator(); i.hasNext(); ) {
				ContentDocument contentDocument = (ContentDocument) i.next();
				String uri = contentDocument.getUri();
				rootSection.addElement(contentDocument, uri.substring(Constants.RESOURCES_URI_PREFIX.length()), localeIdentifier, request);
			}

			// TODO: put in cache here
		}

		// get patterns for index elements
		String patternString = GlobalProperties.getInstance(request.getSession().getServletContext()).getString(Constants.MAP_SECTION_INDEX_PATTERNS_PROPERTY);
		List patternList = new ArrayList();
		if ( !GenericValidator.isBlankOrNull(patternString) ) {
			String[] patterns = patternString.split(",");
			for ( int i = 0; i < patterns.length; i++ ) {
				String pattern = patterns[i];
				if ( !GenericValidator.isBlankOrNull(pattern) ) {
					patternList.add(pattern.trim());
				}
			}
		}

		// recursively set index elements for all sections, remove elements, which are not allowed
		// to currently logged in user and collapse sections which does not have elements
		rootSection = processSection(rootSection, patternList, request);

		// save section in request
		request.setAttribute(WebappConstants.SITE_MAP_KEY, rootSection);

		return mapping.findForward("viewSiteMap");
	}

	/**
	 * Performs recursive copy of section, removes elements which are not allowed
	 * because of security reasons, collapses sections without elements and sets index elements
	 *
	 * @param section  Section to process
	 * @param patterns Patterns to which URI must apply to be recognized as index element
     * @param request Request
	 * @return Copy of section with index element (if any) set and
	 */
	public MapSection processSection(MapSection section, List patterns, HttpServletRequest request) throws IllegalAccessException, NoSuchMethodException, InvocationTargetException {
		MapSection newSection = new MapSection(section.getPath());
		newSection.setIndexElement(null);
		List elements = section.getElements();
		for ( Iterator i = elements.iterator(); i.hasNext(); ) {
			MapSection.SectionElement element = (MapSection.SectionElement) i.next();
			boolean indexElement = false;
			if ( WebappUtil.isUserInRole(element.getRoles(), false, "name", request) ) {
				for ( Iterator j = patterns.iterator(); j.hasNext() && newSection.getIndexElement() == null; ) {
					String pattern = (String) j.next();
					String path = element.getPath();
					if ( matchesPattern(path.substring(section.getPath().length()), pattern) ) {
						newSection.setIndexElement(element);
						indexElement = true;
					}
				}
				if ( !indexElement ) {
					// this element does not match any pattern, so add it to the list
					newSection.getElements().add(element);
				}
			}
		}

		// process subsections recursively
		List sections = section.getSections();

		for ( Iterator i = sections.iterator(); i.hasNext(); ) {
			MapSection subSection = processSection((MapSection) i.next(), patterns, request);
			if ( subSection.getElements().isEmpty() && subSection.getIndexElement() == null ) {
				// move all subsections of this subsection up one level
				for ( Iterator j = subSection.getSections().iterator(); j.hasNext(); ) {
					MapSection s = (MapSection) j.next();
					newSection.addSection(s);
				}
			} else {
				newSection.addSection(subSection);
			}
		}

		// if there is only one element and no index, set this element to be index
		if ( newSection.getElements().size() == 1 && newSection.getIndexElement() == null ) {
			newSection.setIndexElement((MapSection.SectionElement) newSection.getElements().get(0));
			newSection.setElements(new ArrayList());
		}

		return newSection;
	}

	/**
	 * Returns true, if given uri matched pattern
	 *
	 * @param uri     URI to check
	 * @param pattern Pattern to match
	 * @return True if URI matches given pattern, false otherwise
	 */
	protected boolean matchesPattern(String uri, String pattern) {
		// remove extension (if any)
		// query parameters for document URI will be removed too
		int dot = uri.lastIndexOf('.');
		if ( dot != -1 ) {
			uri = uri.substring(0, dot);
		}
		int k = pattern.indexOf('*');
		if ( k == -1 ) {
			// no asterisk provided, full coincidence expected
			return uri.equals(pattern);
		} else {
			if ( k == 0 ) {
				// asterisk is at the first character
				return uri.endsWith(pattern.substring(1));
			} else if ( k == pattern.length() - 1 ) {
				// asterisk is at the last character
				return uri.startsWith(pattern.substring(0, k));
			} else {
				// asterisk is between first and last characters
				String start = pattern.substring(0, k);
				String end = pattern.substring(k + 1, pattern.length());
				return uri.startsWith(start) && uri.endsWith(end);
			}
		}
	}

	/**
	 * Describes section of site map, associated with part of URI of page or document in global page's or document's hierarchy
	 */
	public static class MapSection implements Serializable {

		/**
		 * Path of virtual folders, this section associated with
		 */
		protected String path;

		/**
		 * Index page or document of this section. May be null
		 */
		protected SectionElement indexElement;

		/**
		 * List of subsections
		 */
		protected List sections = new ArrayList();

		/**
		 * List of pages or documents in this section
		 */
		protected List elements = new ArrayList();

		/**
		 * Creates new instance of MapSection
		 *
		 * @param path Path to associate section with
		 */
		public MapSection(String path) {
			if ( path == null ) {
				throw new IllegalArgumentException("Path is null");
			}
			this.path = path;
		}

        /**
         * Returns path
         *
         * @return path
         */
		public String getPath() {
			return path;
		}

        /**
         * Sets path
         *
         * @param path path to set
         */
		public void setPath(String path) {
			this.path = path;
		}

        /**
         * Returns index element
         *
         * @return index element
         */
		public SectionElement getIndexElement() {
			return indexElement;
		}

        /**
         * Sets index element
         *
         * @param indexElement index element to set
         */
		public void setIndexElement(SectionElement indexElement) {
			this.indexElement = indexElement;
		}

        /**
         * Returns sections
         *
         * @return sections
         */
		public List getSections() {
			return sections;
		}

        /**
         * Sets sections
         *
         * @param sections sections to set
         */
		public void setSections(List sections) {
			this.sections = sections;
		}

        /**
         * Returns elements
         *
         * @return elements
         */
		public List getElements() {
			return elements;
		}

        /**
         * Sets elements
         *
         * @param elements elements to set
         */
		public void setElements(List elements) {
			this.elements = elements;
		}

		/**
		 * Adds section to the list
		 *
		 * @param section Section to add
		 */
		public void addSection(MapSection section) {
			sections.add(section);
		}

		/**
		 * Adds object to this section. Creates sub section if necessary
		 *
		 * @param obj              Object to add
		 * @param path             Path to section, which must contain this object
		 * @param localeIdentifier Identifier of locale to select title
		 * @param request          HTTP servlet request we're currently proceeding
		 */
		public void addElement(BaseObject obj, String path, String localeIdentifier, HttpServletRequest request) {

			// find slash after path, associated with this section
			int k = path.indexOf("/", this.path.length());
			if ( k != -1 ) {
				// path to child section. includes path of this section,
				// folder name following this path and slash at the end of string
				String sectionPath = path.substring(0, k + 1);
				// create sub section
				MapSection section = findSection(sectionPath);
				if ( section == null ) {
					// section not found: creating new
					section = new MapSection(sectionPath);
					addSection(section);
				}
				// add this object to child section
				section.addElement(obj, path, localeIdentifier, request);
			} else {
				// add object to this section
				SectionElement se = new SectionElement(obj, path, localeIdentifier, request);
				elements.add(se);
			}
		}

		/**
		 * Searches section by specified path
		 *
		 * @param path Path to search section by
		 * @return Section or null if none found
		 */
		protected MapSection findSection(String path) {
			for ( Iterator i = sections.iterator(); i.hasNext(); ) {
				MapSection section = (MapSection) i.next();
				if ( section.getPath().equals(path) ) {
					return section;
				}
			}
			return null;
		}

		public boolean equals(Object o) {
			if ( this == o ) {
				return true;
			}
			if ( !(o instanceof MapSection) ) {
				return false;
			}

			final MapSection mapSection = (MapSection) o;

			if ( path != null ? !path.equals(mapSection.path) : mapSection.path != null ) {
				return false;
			}

			return true;
		}

		public int hashCode() {
			return (path != null ? path.hashCode() : 0);
		}

		/**
		 * Wrapper class that describes separate section element
		 */
		public static class SectionElement implements Serializable {

			public static final String SECTION_ELEMENT_PAGE = "page";
			public static final String SECTION_ELEMENT_DOCUMENT = "document";

			/**
			 * Title of this element
			 */
			protected String title;

            /**
             * Path that correspond to element
             */
            protected String path;

			/**
			 * URI of this element
			 */
			protected String uri;

			/**
			 * Allowed roles
			 */
			protected List roles;

			/**
			 * Type of element
			 */
			protected String type;

			/**
			 * Creates new instance of SectionElement
			 *
			 * @param obj              Object to wrap
             * @param path             Path that object has
             * @param localeIdentifier Identifier of locale to get titles of pages for
             * @param request          HTTP servelt request we're currently proceeding
             */
			public SectionElement(BaseObject obj, String path,
                                  String localeIdentifier,
                                  HttpServletRequest request) {
				if ( !(obj instanceof ContentPage) && !(obj instanceof ActionPage) && !(obj instanceof ContentDocument) ) {
					throw new IllegalArgumentException("Cannot wrap instance of " + obj.getClass().getName());
				}

				if ( obj instanceof Page ) {
					Page page = (Page) obj;
					title = StringUtil.shortenString(WebappUtil.getFieldValue(page.getTitle(), localeIdentifier, request, true), 40);
					uri = WebappUtil.getActionMappingURL(page.getUri(), null, request, WebappConstants.URL_TYPE_CONTEXT_RELATIVE);
					if ( GenericValidator.isBlankOrNull(title) ) {
						title = uri;
					}
					type = SECTION_ELEMENT_PAGE;
					if ( page instanceof ContentPage ) {
						ContentPage cp = (ContentPage) page;
						roles = cp.getRoles();
					} else if ( page instanceof ActionPage ) {
						ActionPage ap = (ActionPage) page;
						roles = WebappUtil.getAPRoleNamesAsList(ap.getUri(), request);
					}
				} else {
					ContentDocument document = (ContentDocument) obj;
                    String description = WebappUtil.getFieldValue(document.getDescription(),
                                                                  localeIdentifier,
                                                                  request,
                                                                  true);
                    if ( !GenericValidator.isBlankOrNull(description) ) {
						title = StringUtil.shortenString(description, 40);
					} else {
						title = document.getUri();
					}
					uri = document.getUri() + "?view=true";
					roles = document.getRoles();
					type = SECTION_ELEMENT_DOCUMENT;
				}

				if ( roles == null ) {
					roles = new ArrayList();
				}

                this.path = path;
			}

            /**
             * Returns title
             *
             * @return title
             */
			public String getTitle() {
				return title;
			}

            /**
             * Sets title
             *
             * @param title title to set
             */
			public void setTitle(String title) {
				this.title = title;
			}

            /**
             * Returns path.
             *
             * @return path
             */
            public String getPath() {
                return path;
            }

            /**
             * Sets path.
             *
             * @param path path to set
             */
            public void setPath(String path) {
                this.path = path;
            }

            /**
             * Returns URI
             *
             * @return URI
             */
			public String getUri() {
				return uri;
			}

            /**
             * Sets URI
             *
             * @param uri URI to set
             */
			public void setUri(String uri) {
				this.uri = uri;
			}

            /**
             * Returns roles
             *
             * @return roles
             */
			public List getRoles() {
				return roles;
			}

            /**
             * Sets roles
             *
             * @param roles roles to set
             */
			public void setRoles(List roles) {
				this.roles = roles;
			}

            /**
             * Returns type
             *
             * @return type
             */
			public String getType() {
				return type;
			}

            /**
             * Sets type
             *
             * @param type type to set
             */
			public void setType(String type) {
				this.type = type;
			}

			public boolean equals(Object o) {
				if ( this == o ) {
					return true;
				}
				if ( !(o instanceof SectionElement) ) {
					return false;
				}

				final SectionElement sectionElement = (SectionElement) o;

				if ( !type.equals(sectionElement.type) ) {
					return false;
				}
				if ( !uri.equals(sectionElement.uri) ) {
					return false;
				}

				return true;
			}

			public int hashCode() {
				int result;
				result = uri.hashCode();
				result = 29 * result + type.hashCode();
				return result;
			}

		}
	}

}